/*
 * Copyright 2007 Paul Hammant
 * Copyright 2007 ThinkTank Maths Limited
 * 
 * ThinkTank Maths Limited grants a non-revocable, perpetual licence
 * to Paul Hammant for unlimited use, relicensing and redistribution. No
 * explicit permission is required from ThinkTank Maths Limited for
 * any future decisions made with regard to this file.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package cn.org.rapid_framework.generator.util.paranamer;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Implementation of {@link Paranamer} which can access Javadocs at runtime to extract
 * parameter names of methods. Works with:-
 * <ul>
 * <li>Javadoc in zip file</li>
 * <li>Javadoc in directory</li>
 * <li>Javadoc at remote URL</li>
 * </ul>
 * Future implementations may be able to take multiple sources, but this version must be
 * instantiated with the correct location of the Javadocs for the package you wish to
 * extract the parameter names. Note that if a zip archive contains multiple
 * "package-list" files, the first one will be used to index the packages which may be
 * queried.
 * <p>
 * Note that this does not perform any caching of entries (except what it finds in the
 * package-list file, which is very lightweight)... every lookup will involve a disc hit.
 * If you want to speed up performance, use a {@link CachingParanamer}.
 * <p>
 * Implementation note: the constructors of this implementation let the client know if I/O
 * problems will stop the recovery of parameter names. It might be preferable to suppress
 * exceptions and simply return NO_PARAMETER_NAMES_LIST.
 * <p>
 * TODO: example use code
 * <p>
 * Known issues:-
 * <ul>
 * <li>Only tested with Javadoc 1.3 - 1.6</li>
 * <li>Doesn't handle methods that declare the generic type as a parameter (rare use case)</li>
 * <li>Some "erased" generic methods fail, e.g. File.compareTo(File), which is erased to
 * File.compareTo(Object).</li>
 * <li>URL implementation is really slow</li>
 * <li>Doesn't support nested classes (due to limitations in the Java 1.4 reflection API)</li>
 * </ul>
 * 
 * @author Samuel Halliday, ThinkTank Maths Limited
 */
public class JavadocParanamer implements Paranamer {

    private static final String IE = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)";

    private static final ParameterNamesNotFoundException CLASS_NOT_SUPPORTED = new ParameterNamesNotFoundException(
	    "class not supported");

    /** In the case of an archive, this stores the path up to the base of the Javadocs */
    private String base = null;
    private final boolean isArchive;
    private final boolean isDirectory;

    private final boolean isURI;

    /**
     * Regardless of the implementation, this stores the base location of the remote or
     * local file or directory.
     */
    private final URI location;

    /** The packages which are supported by this instance. Contains Strings */
    private final Set<String> packages = new HashSet<String>();

    /**
     * Construct a Javadoc reading implementation of {@link Paranamer} using a local
     * directory or zip archive as a source.
     * 
     * @param archiveOrDirectory
     *            either a zip archive of Javadocs or the base directory of Javadocs.
     * @throws IOException
     *             if there was an error when reading from either the archive or the
     *             package-list file.
     * @throws FileNotFoundException
     *             if the archive, directory or <code>package-list</code> file does not
     *             exist.
     * @throws NullPointerException
     *             if any parameter is null
     * @throws IllegalArgumentException
     *             If the given parameter is not a file or directory or if it is a file
     *             but not a javadoc zip archive.
     */
    public JavadocParanamer(File archiveOrDirectory) throws IOException {

	if (archiveOrDirectory==null)
	    throw new NullPointerException();

	if (!archiveOrDirectory.exists())
	    throw new FileNotFoundException(archiveOrDirectory.getAbsolutePath());

	isURI = false;
	location = archiveOrDirectory.toURI();

	if (archiveOrDirectory.isDirectory()) {
	    // is a directory
	    isArchive = false;
	    isDirectory = true;
	    // check that "package-list" exists
	    File dir = archiveOrDirectory;
	    File packageList = new File(dir.getAbsolutePath()+"/package-list");
	    if (!packageList.isFile())
		throw new FileNotFoundException("No package-list found at "+dir.getAbsolutePath()
			+". Not a valid Javadoc directory.");
	    // it appear to be a valid Javadoc directory
	    FileInputStream input = new FileInputStream(packageList);
	    try {
		String packageListString = streamToString(input);
		parsePackageList(packageListString);
	    } finally {
		input.close();
	    }
	} else if (archiveOrDirectory.isFile()) {
	    // is a file
	    isArchive = true;
	    isDirectory = false;
	    File archive = archiveOrDirectory;
	    if (!archive.getAbsolutePath().toLowerCase().endsWith(".zip"))
		throw new IllegalArgumentException(archive.getAbsolutePath()+" is not a zip file.");
	    // check that a "package-list" exists somewhere in the archive
	    ZipFile zip = new ZipFile(archive);
	    try {
		// we need to check for a file named "package-list".
		// There may be multiple files in the archive
		// but we cannot use ZipFile.getEntry for suffix names
		// so we have to look through all the entries.
		// We then pick the largest file.
		Enumeration<? extends ZipEntry> entries = zip.entries();
		// grr...
		// http://javablog.co.uk/2007/11/25/enumeration-and-iterable
		// Set<ZipEntry>
		SortedMap<Long, ZipEntry> packageLists = new TreeMap<Long, ZipEntry>();
		while (entries.hasMoreElements()) {
		    ZipEntry entry = entries.nextElement();
		    String name = entry.getName();
		    if (name.endsWith("package-list")) {
			Long size = entry.getSize();
			packageLists.put(size, entry);
		    }
		}
		if (packageLists.size()==0)
		    throw new FileNotFoundException("no package-list found in archive");

		// pick the largest package-list file, it's most likely the one
		// we want
		ZipEntry entry = packageLists.get(packageLists.lastKey());
		String name = entry.getName();
		base = name.substring(0, name.length()-"package-list".length());
		InputStream input = zip.getInputStream(entry);
		try {
		    String packageListString = streamToString(input);
		    parsePackageList(packageListString);
		} finally {
		    input.close();
		}
	    } finally {
		zip.close();
	    }
	} else
	    throw new IllegalArgumentException(archiveOrDirectory.getAbsolutePath()
		    +" is neither a directory nor a file.");
    }

    /**
     * @param url The URL of the JavaDoc
     * @throws IOException
     *             if there was a problem connecting to the remote Javadocs
     * @throws FileNotFoundException
     *             if the url does not have a <code>/package-list</code>
     * @throws NullPointerException
     *             if any parameter is null
     */
    public JavadocParanamer(URL url) throws IOException {

	if (url==null)
	    throw new NullPointerException();

	isArchive = false;
	isDirectory = false;
	isURI = true;
	try {
	    location = new URI(url.toString());
	} catch (URISyntaxException e) {
	    throw new IOException(e.getMessage());
	}

	// check the package-list
	URL packageListURL = new URL(url.toString()+"/package-list");
	InputStream input = urlToInputStream(packageListURL);
	try {
	    String packageList = streamToString(input);
	    parsePackageList(packageList);
	} finally {
	    input.close();
	}
    }

    public String[] lookupParameterNames(AccessibleObject methodOrConstructor) {

	return lookupParameterNames(methodOrConstructor, true);
    }

    public String[] lookupParameterNames(AccessibleObject methodOrConstructor,
	    boolean throwExceptionIfMissing) {

	if (methodOrConstructor==null)
	    throw new NullPointerException();

	Class<?> klass;
	String name;
	Class<?>[] types;

	if (methodOrConstructor instanceof Constructor<?>) {
	    Constructor<?> constructor = (Constructor<?>) methodOrConstructor;
	    klass = constructor.getDeclaringClass();
	    name = constructor.getName();
	    types = constructor.getParameterTypes();
	} else if (methodOrConstructor instanceof Method) {
	    Method method = (Method) methodOrConstructor;
	    klass = method.getDeclaringClass();
	    name = method.getName();
	    types = method.getParameterTypes();
	} else
	    throw new IllegalArgumentException();

	// quick check to see if we support the package
	if (!packages.contains(klass.getPackage().getName()))
	    throw CLASS_NOT_SUPPORTED;

	try {
	    String[] names = getParameterNames(klass, name, types);
	    if (names==null) {
		if (throwExceptionIfMissing) {
		    throw new ParameterNamesNotFoundException(methodOrConstructor.toString());
		} else {
		    return Paranamer.EMPTY_NAMES;
		}
	    }
	    return names;
	} catch (IOException e) {
	    if (throwExceptionIfMissing) {
		throw new ParameterNamesNotFoundException(methodOrConstructor.toString()
			+" due to an I/O error: "+e.getMessage());
	    } else {
		return Paranamer.EMPTY_NAMES;
	    }
	}
    }

    // throws CLASS_NOT_SUPPORTED if the class file is not found in the javadocs
    // return null if the parameter names were not found
    private String[] getParameterNames(Class<?> klass, String constructorOrMethodName,
	    Class<?>[] types) throws IOException {

	// silly request for names of a parameterless method/constructor!
	if ((types!=null)&&(types.length==0))
	    return new String[0];

	String path = getCanonicalName(klass).replace('.', '/');
	if (isArchive) {
	    ZipFile archive = new ZipFile(new File(location));
	    ZipEntry entry = archive.getEntry(base+path+".html");
	    if (entry==null)
		throw CLASS_NOT_SUPPORTED;
	    InputStream input = archive.getInputStream(entry);
	    return getParameterNames2(input, constructorOrMethodName, types);
	} else if (isDirectory) {
	    File file = new File(location.getPath()+"/"+path+".html");
	    if (!file.isFile())
		throw CLASS_NOT_SUPPORTED;
	    FileInputStream input = new FileInputStream(file);
	    return getParameterNames2(input, constructorOrMethodName, types);
	} else if (isURI) {
	    try {
		URL url = new URL(location.toString()+"/"+path+".html");
		InputStream input = urlToInputStream(url);
		return getParameterNames2(input, constructorOrMethodName, types);
	    } catch (FileNotFoundException e) {
		throw CLASS_NOT_SUPPORTED;
	    }
	}
	throw new RuntimeException("bug in JavadocParanamer. Should not reach here.");
    }

    /*
     * Parse the Javadoc String and return the parameter names for the given constructor
     * or method. Return null if no method/constructor is found. Note that types will
     * never have length zero... we already deal with that situation higher up in the
     * chain. Don't forget to close the input!
     */
    private String[] getParameterNames2(InputStream input, String constructorOrMethodName,
	    Class<?>[] types) throws IOException {

	String javadoc = streamToString(input);
	input.close();

	// String we're looking for is like
	//
	// NAME="constructorOrMethodName(obj.ClassName, ...)"...noise...
	// <DT><B>Parameters:</B><DD><CODE>parameter_name_1</CODE>...noise...
	// <DD><CODE>parameter_name_2</CODE>...noise...
	// ...
	// <DD><CODE>parameter_name_N</CODE>...noise...
	//
	// We cannot rely on the Parameters line existing as it depends on the
	// author
	// having correctly marked-up their code. The NAME element is
	// auto-generated
	// and should be checked for aggressively.
	//
	// Also note that Javadoc parameter names may differ from the names in
	// the source.

	// we don't have Pattern/Matcher :-(
	StringBuffer regex = new StringBuffer();
	regex.append("NAME=\"");
	regex.append(constructorOrMethodName);
	// quotes needed to escape array brackets
	regex.append("\\(\\Q");
	for (int i = 0; i<types.length; i++) {
	    if (i!=0)
		regex.append(", ");
	    // canonical name deals with arrays
	    regex.append(getCanonicalName(types[i]));
	}
	regex.append("\\E\\)\"");

	// FIXME: handle Javadoc 1.3, 1.4 and 1.5 as well (this is 1.6)

	Pattern pattern = Pattern.compile(regex.toString());
	Matcher matcher = pattern.matcher(javadoc);
	if (!matcher.find())
	    // not found
	    return Paranamer.EMPTY_NAMES;

	// found it. Lookup the parameter names.
	String[] names = new String[types.length];
	// now we're sure we have the right method, find the parameter names!
	String regexParams = "<DD><CODE>([^<]*)</CODE>";
	Pattern patternParams = Pattern.compile(regexParams);
	int start = matcher.end();
	Matcher matcherParams = patternParams.matcher(javadoc);
	for (int i = 0; i<types.length; i++) {
	    boolean find = matcherParams.find(start);
	    if (!find)
		return Paranamer.EMPTY_NAMES;
	    start = matcherParams.end();
	    names[i] = matcherParams.group(1);
	}
	return names;
    }

    // doesn't support names of nested classes
    private String getCanonicalName(Class<?> klass) {

	if (klass.isArray())
	    return getCanonicalName(klass.getComponentType())+"[]";

	return klass.getName();
    }

    // storing the list of packages that we support is very lightweight
    private void parsePackageList(String packageList) throws IOException {

	StringReader reader = new StringReader(packageList);
	BufferedReader breader = new BufferedReader(reader);
	String line;
	while ((line = breader.readLine())!=null) {
	    packages.add(line);
	}
    }

    // read an InputStream into a UTF-8 String
    private String streamToString(InputStream input) throws IOException {

	InputStreamReader reader;
	try {
	    reader = new InputStreamReader(input, "UTF-8");
	} catch (UnsupportedEncodingException e) {
	    // this should never happen
	    reader = new InputStreamReader(input);
	}
	BufferedReader breader = new BufferedReader(reader);
	String line;
	StringBuffer builder = new StringBuffer();
	while ((line = breader.readLine())!=null) {
	    builder.append(line);
	    builder.append("\n");
	}
	return builder.toString();
    }

    private InputStream urlToInputStream(URL url) throws IOException {

	URLConnection conn = url.openConnection();
	// pretend to be IE6
	conn.setRequestProperty("User-Agent", IE);
	// allow both GZip and Deflate (ZLib) encodings
	conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
	conn.connect();
	String encoding = conn.getContentEncoding();
	if ((encoding!=null)&&encoding.equalsIgnoreCase("gzip"))
	    return new GZIPInputStream(conn.getInputStream());
	else if ((encoding!=null)&&encoding.equalsIgnoreCase("deflate"))
	    return new InflaterInputStream(conn.getInputStream(), new Inflater(true));
	else
	    return conn.getInputStream();
    }

}
